﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PSE.Deployment.Engine.Interfaces;
using PSE.Deployment.Engine.CustomEventArgs;
using PSE.Deployment.Engine.Enums;
using System.Threading;
using PSE.Deployment.Engine.CustomExceptions;
using System.IO;

namespace PSE.Deployment.Engine
{
    public abstract class BaseStepGroup : BaseStep, IStepGroup
    {
        #region Propriedades relacionadas a gravacao de log
        private string _dateFormat = "dd/MM/yyyy HH:mm:ss";
        //propriedade utilizada para montar o nome do arquivo
        protected static DateTime ExecutionDateTime
        {
            get;
            set;
        }
        //criada variavel estatica para que todos os grupos tenham o mesmo logPath
        private static string _logPath;
        //mantida propriedade de instancia para manter compatibilidade
        public string LogPath
        {
            get
            {
                return _logPath;
            }
            set
            {
                _logPath = value;
            }
        }
        #endregion

        #region Propriedades
        public StepCollection Steps
        { get; set; }

        public StepCollection ExecutedSteps
        { get; set; }

        public StepCollection IgnoredSteps
        { get; set; }


        public StepGroupCancelBehavior CancelBehavior
        {
            get;
            set;
        }

        private ManualResetEvent _executionStoped;
        private ManualResetEvent _navigatePrevious;
        #endregion

        #region Construtores
        public BaseStepGroup(IStepGroup parent)
            : base(parent)
        {
            Steps = new StepCollection();
            IgnoredSteps = new StepCollection();
            ExecutedSteps = new StepCollection();
        }
        #endregion

        #region Eventos
        public event EventHandler<ExecutionCanceledEventArgs> ExecutionCanceled;
        #endregion

        #region Metodos publicos

        /// <summary>
        /// Executar os steps de um stepGroup
        /// </summary>
        /// <param name=""></param>
        public override void Execute()
        {
            this.ExecutedSteps = new StepCollection();
            this.IgnoredSteps = new StepCollection();
            this._executionStoped = new ManualResetEvent(false);
            this._navigatePrevious = new ManualResetEvent(false);

            ExecuteSteps(this.Steps);

            //voltar entre grupos nao sera suportado nessa versao
            //if (_navigatePrevious.WaitOne(1))
            //{
            //    this.Undo();
            //    if (this.Parent != null)
            //        this.Parent.GoToPrevious();
            //}
        }

        /// <summary>
        /// Cancelar a execução do Grupo
        /// </summary>
        public void Cancel()
        {
            _executionStoped.Set();
            this.ExecutionStatus = StepExecutionStatus.Canceled;

            this.Undo();

            ExecutionCanceledEventArgs e = new ExecutionCanceledEventArgs(this);
            //sempre subir o evento para avisar do cancelamento do grupo
            this.OnExecutionCanceled(e);

            switch (this.CancelBehavior)
            {
                case StepGroupCancelBehavior.NotifyParent:
                    if (e.CancelAction == StepGroupCancelAction.CancelParent)
                    {
                        //se ha um parent
                        if (this.Parent != null)
                            this.Parent.Cancel();
                    }
                    else if (e.CancelAction == StepGroupCancelAction.StopParent)
                    {
                        //se há um parent
                        if (this.Parent != null)
                            this.Parent.Stop();
                    }
                    break;
                case StepGroupCancelBehavior.CancelParent:
                    if (this.Parent != null)
                        this.Parent.Cancel();
                    break;
                case StepGroupCancelBehavior.StopParent:
                    if (this.Parent != null)
                        this.Parent.Stop();
                    break;
            }
        }

        /// <summary>
        /// Cancelar a execução do Grupo
        /// </summary>
        public void Stop()
        {
            _executionStoped.Set();
            this.ExecutionStatus = StepExecutionStatus.Finished;
            //se houver um parent, parar a execução
            if (this.Parent != null)
                this.Parent.Stop();
        }


        /// <summary>
        /// Desfazer os passos realizados
        /// </summary>
        /// <param name=""></param>
        public override void Undo()
        {
            this.ExecutionStatus = StepExecutionStatus.Canceled;
            if (this.ExecutedSteps != null)
            {
                this.OnUndoStarted(new UndoStartedEventArgs(this));

                for (int i = this.ExecutedSteps.Count - 1; i >= 0; i--)
                {
                    IStep step = this.ExecutedSteps[i];
                    step.ExecutionStatus = StepExecutionStatus.Canceled;

                    UndoStep(step);
                    this.ExecutedSteps.Remove(step);
                }

                foreach (IStep step in this.Steps)
                {
                    step.ExecutionStatus = StepExecutionStatus.Canceled;
                }

                this.OnUndoFinished(new UndoFinishedEventArgs(this));
            }
        }

        /// <summary>
        /// Executar uma sequencia de passos
        /// </summary>
        /// <param name=""></param>
        /// <param name="steps"></param>
        private void ExecuteSteps(StepCollection steps)
        {
            //foreach (IStep step in this.Steps)
            for (int i = 0; i < this.Steps.Count; )
            {
                IStep step = this.Steps[i];
                AttachEvents(step);
                ExecuteStep(step);

                //se a execução foi finalizada
                if (_executionStoped.WaitOne(1))
                {
                    //marcar os demais passos como cancelados
                    for (int j = i + 1; j < this.Steps.Count; j++)
                    {
                        this.Steps[j].ExecutionStatus = StepExecutionStatus.Canceled;
                    }

                    _executionStoped.Reset();
                    break;
                }
                //se a foi indicado para navegar para o anterior
                else if (_navigatePrevious.WaitOne(1))
                {
                    //caso o passo tenha sido executado
                    if (this.ExecutedSteps.Contains(step.Id))
                    {
                        //desfazer e remover da lista de passos executados
                        UndoStep(step);
                        this.ExecutedSteps.Remove(step.Id);//remover da lista de executados                    
                    }

                    _navigatePrevious.Reset();

                    //se for o primeiro step da lista de steps
                    if (i == 0)
                    {
                        //se existe um parent
                        if (step.Parent != null)
                        {
                            //solicitar ao parente que retorne ao intem anterior
                            step.Parent.GoToPrevious();
                            break;
                        }
                        else
                        {
                            //TODO:traduzir
                            throw new InvalidUsageException("Não há grupo ou passo anterior para ser executado.");
                        }
                    }
                    else
                    {
                        i--;
                    }
                }
                //caso nao tenha sido cancelado, e nao tenha sido indicado para voltar o passo anterior
                //executar o proximo passo
                else
                {
                    i++;
                }
            }
        }

        private void AttachEvents(IStep step)
        {
            //TODO: melhorar forma e tratar os eventos, ao inves de sempre fazer os Dettach e Attach, guardar um estado
            //de que ja foi realizado o Attach ou algo do tipo

            step.ExecutionStarted -= new EventHandler<ExecutionStartedEventArgs>(step_OnExecuteStarted);
            step.ExecutionStarted += new EventHandler<ExecutionStartedEventArgs>(step_OnExecuteStarted);

            step.ExecutionFinished -= new EventHandler<ExecutionFinishedEventArgs>(step_OnExecuteFinished);
            step.ExecutionFinished += new EventHandler<ExecutionFinishedEventArgs>(step_OnExecuteFinished);

            step.ExecutionError -= new EventHandler<ExecutionErrorEventArgs>(step_OnExecuteError);
            step.ExecutionError += new EventHandler<ExecutionErrorEventArgs>(step_OnExecuteError);

            step.ExecutionIgnored -= new EventHandler<ExecutionIgnoredEventArgs>(step_ExecuteIgnored);
            step.ExecutionIgnored += new EventHandler<ExecutionIgnoredEventArgs>(step_ExecuteIgnored);

            step.UndoStarted -= new EventHandler<UndoStartedEventArgs>(step_UndoStarted);
            step.UndoStarted += new EventHandler<UndoStartedEventArgs>(step_UndoStarted);

            step.UndoFinished -= new EventHandler<UndoFinishedEventArgs>(step_UndoFinished);
            step.UndoFinished += new EventHandler<UndoFinishedEventArgs>(step_UndoFinished);

            step.UndoError -= new EventHandler<UndoErrorEventArgs>(step_UndoError);
            step.UndoError += new EventHandler<UndoErrorEventArgs>(step_UndoError);

            step.ExecutionProgress -= new EventHandler<ExecutionProgressEventArgs>(step_ExecutionProgress);
            step.ExecutionProgress += new EventHandler<ExecutionProgressEventArgs>(step_ExecutionProgress);

            if (step is IStepGroup)
            {
                ((IStepGroup)step).ExecutionCanceled -= new EventHandler<ExecutionCanceledEventArgs>(BaseStepGroup_Canceled);
                ((IStepGroup)step).ExecutionCanceled += new EventHandler<ExecutionCanceledEventArgs>(BaseStepGroup_Canceled);
            }
        }

        void step_UndoStarted(object sender, UndoStartedEventArgs e)
        {
            if (e.OriginalSource == sender)
            {
                this.OnUndoStarted(e);
            }
            else
            {
                this.OnUndoStarted(sender, e);
            }
        }

        void step_UndoFinished(object sender, UndoFinishedEventArgs e)
        {
            if (e.OriginalSource == sender)
            {
                this.OnUndoFinished(e);
            }
            else
            {
                this.OnUndoFinished(sender, e);
            }
        }

        void step_UndoError(object sender, UndoErrorEventArgs e)
        {
            if (e.OriginalSource == sender)
            {
                this.OnUndoError(e);
            }
            else
            {
                this.OnUndoError(sender, e);
            }
        }

        void step_ExecuteIgnored(object sender, ExecutionIgnoredEventArgs e)
        {
            //apenas repassar o evento
            this.OnExecutionIgnored(sender, e);
        }

        void BaseStepGroup_Canceled(object sender, ExecutionCanceledEventArgs e)
        {
            this.OnExecutionCanceled(sender, e);
        }

        void step_ExecutionProgress(object sender, ExecutionProgressEventArgs e)
        {
            if (e.OriginalSource == sender)
            {
                this.OnExecutionProgress(e);
            }
            else
            {
                this.OnExecutionProgress(sender, e);
            }
        }


        /// <summary>
        /// Controlar o numero de vezes que um passo foi re-executado
        /// </summary>
        private int _stepExecutionCount = 0;

        /// <summary>
        /// Executar um passo/grupo
        /// </summary>
        /// <param name="step"></param>
        /// <param name=""></param>
        private void ExecuteStep(IStep step)
        {
            //caso o passo ja tenha sido executado, desfazer
            if (this.ExecutedSteps.Contains(step.Id))
            {
                UndoStep(step);
                this.ExecutedSteps.Remove(step.Id);
            }
            //caso o passo tenha sido ignorado, remover da lista pois o mesmo sera executado
            else if (this.IgnoredSteps.Contains(step.Id))
            {
                this.IgnoredSteps.Remove(step.Id);
            }

            //notificar a inicio da execucao do passo
            step.OnExecutionStarted(new ExecutionStartedEventArgs(step));

            //inicializar o estado do step
            step.ExecutionException = null;
            step.ExecutionStatus = StepExecutionStatus.Finished;
            try
            {
                _stepExecutionCount++;
                step.Execute();

                if (step.ExecutionStatus == StepExecutionStatus.Finished)
                {
                    step.OnExecutionFinished(new ExecutionFinishedEventArgs(step));
                }
            }
            catch (InvalidUsageException)
            {
                //se ocorreu um exception de uso incorreto do engine subir o erro
                throw;
            }
            catch (Exception ex)
            {
                //se o passo esta configurado para ser re-executado automaticamente no caso de erro
                if (step.AutomaticRetryOnError && step.MaxAttempts > 0 && step.MaxAttempts > _stepExecutionCount)
                {
                    if (step.RetryMillisecondsTimeout > 0)
                        System.Threading.Thread.Sleep(step.RetryMillisecondsTimeout);

                    ExecuteStep(step);
                }
                else
                {
                    //logar o erro ocorrido no passo.
                    //movido para essa classe para resolver problema de exibição de messagebox de erro antes
                    //de logar o erro
                    WriteStepLog(step, "finalizado com erros.", ex);

                    step.ExecutionStatus = StepExecutionStatus.Error;
                    step.ExecutionException = ex;
                    step.OnExecutionError(new ExecutionErrorEventArgs(step, ex));
                }
            }
            finally
            {
                _stepExecutionCount = 0;
            }

        }

        private void UndoStep(IStep step)
        {
            //notificar a inicio da execucao do undo
            step.OnUndoStarted(new UndoStartedEventArgs(step));
            try
            {
                step.Undo();
                step.OnUndoFinished(new UndoFinishedEventArgs(step));
            }
            catch (Exception ex)
            {
                step.UndoException = ex;
                step.OnUndoError(new UndoErrorEventArgs(step, step.UndoException));
            }
        }

        void step_OnExecuteStarted(object sender, ExecutionStartedEventArgs e)
        {
            //subir o evento
            this.OnExecutionStarted(sender, e);
        }

        void step_OnExecuteFinished(object sender, ExecutionFinishedEventArgs e)
        {
            if (e.OriginalSource == sender)
            {
                //subir o evento
                this.OnExecutionFinished(e);

                IStep step = sender as IStep;

                this.ExecutedSteps.Add(step);
                switch (e.NavigationAction)
                {
                    case StepNavigationAction.Previous:
                        if (!(step.NavigationBehavior == StepNavigationBehavior.NextPreviousCancel))
                        {
                            ///operação inválida executada o passo nao está configurado para voltar
                            throw new InvalidUsageException(string.Format("Não é permitido voltar a execução do passo {0}. Pois o passo {0} não suporta essa operação.", step.Id));
                        }
                        GoToPrevious();
                        break;
                    case StepNavigationAction.Cancel:
                        this.Cancel();
                        break;
                }
            }
            else
            {
                this.OnExecutionFinished(sender, e);
            }

        }

        public void GoToPrevious()
        {
            //indicar que devera voltar para o passo anterior
            _navigatePrevious.Set();
        }

        void step_OnExecuteError(object sender, ExecutionErrorEventArgs e)
        {
            //se o source do erro for o objeto que disparou o evento
            if (e.OriginalSource == sender)
            {
                IStep step = sender as IStep;

                //sempre indicar que houve um erro no grupo
                this.OnExecutionError(e);

                //desfazer o passo no qual ocorreu o erro
                UndoStep(step);

                switch (step.ErrorBehavior)
                {
                    case StepErrorBehavior.NotifyParent:
                        switch (e.ErrorAction)
                        {
                            case StepErrorAction.TryAgain:
                                ExecuteStep(step);
                                break;
                            case StepErrorAction.CancelParent:
                                this.Cancel();
                                break;
                            case StepErrorAction.Ignore:
                                if (!step.Ignorable)
                                {
                                    //TODO: Traduzir para inglês
                                    throw new InvalidUsageException(string.Format("O passo {0} não pode ser ignorado, pois esse comportamento não é suportado.", step.Id));
                                }
                                this.IgnoreStep(step);
                                break;
                        }

                        break;
                    case StepErrorBehavior.CancelParent:
                        this.Cancel();
                        break;
                    case StepErrorBehavior.Ignore:
                        this.IgnoreStep(step);
                        break;
                }
            }
            //apenas repassar o evento
            else
            {
                this.OnExecutionError(sender, e);
            }
        }

        /// <summary>
        /// Ignorar a execução de um Step de um StepGroup
        /// Deverá registar que esse step foi ignorado para ser apresentado ao final da execução do
        /// step
        /// </summary>
        private void IgnoreStep(IStep step)
        {
            this.IgnoredSteps.Add(step);
            this.OnExecutionIgnored(step, new ExecutionIgnoredEventArgs(step));
        }


        public void OnExecutionCanceled(ExecutionCanceledEventArgs e)
        {
            this.OnExecutionCanceled(this, e);
        }

        public void OnExecutionCanceled(object sender, ExecutionCanceledEventArgs e)
        {
            if (this.ExecutionCanceled != null)
                this.ExecutionCanceled(sender, e);
        }

        #endregion

        #region LOG
        protected string GetFullStepName(IStep step)
        {
            string fullName = "";
            if (step.Parent != null)
            {
                fullName = GetFullStepName(step.Parent) + "." + step.Id;
                return fullName;
            }
            else
            {
                return step.Id;
            }
        }

        public void WriteStepLog(string logText)
        {
            string logFilePath = this.LogPath;

            //caso nao seja informado um diretorio
            if (!String.IsNullOrEmpty(logFilePath))
            {
                if (!Directory.Exists(logFilePath))
                {
                    try
                    {
                        Directory.CreateDirectory(logFilePath);
                    }
                    catch
                    {
                        //caso n seja possivel criar o diretorio para armazenar o log, não gerar o arquivo de log
                        return;
                    }
                }

                logFilePath += @"\" + string.Format("Log_{0}.txt", ExecutionDateTime.ToString("yyyyMMddHHmmss"));

                StreamWriter sw = new StreamWriter(logFilePath, true);
                sw.Write(logText);
                sw.Close();

                Console.WriteLine(logText);
            }
            else
            {
                throw new InvalidOperationException("Cannot write log, LogPath is empty.");
            }
        }

        public void WriteStepLog(IStep step, string logText)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine(string.Format("{0} - {1} {2}", DateTime.Now.ToString(_dateFormat), GetFullStepName(step), logText));

            WriteStepLog(sb.ToString());
        }

        public void WriteStepLog(IStep step, string logText, Exception ex)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(logText);

            if (ex != null)
            {
                sb.AppendLine();
                sb.AppendLine("-----------------------------------------------------------------------------");
                sb.AppendLine(string.Format("Detalhes: {0}", ex.ToString()));
                sb.AppendLine("-----------------------------------------------------------------------------");
            }

            WriteStepLog(step, sb.ToString());
        }
        #endregion
    }
}
